// 版权所有2011 Go作者。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// Package get实现“go get”命令。
package get

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	"cmd/go/internal/base"
	"cmd/go/internal/cfg"
	"cmd/go/internal/load"
	"cmd/go/internal/search"
	"cmd/go/internal/str"
	"cmd/go/internal/vcs"
	"cmd/go/internal/web"
	"cmd/go/internal/work"

	"golang.org/x/mod/module"
)

var CmdGet = &base.Command{
	UsageLine: "go get [-d] [-f] [-t] [-u] [-v] [-fix] [build flags] [packages]",
	Short:     "download and install packages and dependencies",
	Long: `
Get downloads the packages named by the import paths, along with their
dependencies. It then installs the named packages, like 'go install'.

The -d flag instructs get to stop after downloading the packages; that is,
it instructs get not to install the packages.

The -f flag, valid only when -u is set, forces get -u not to verify that
each package has been checked out from the source control repository
implied by its import path. This can be useful if the source is a local fork
of the original.

The -fix flag instructs get to run the fix tool on the downloaded packages
before resolving dependencies or building the code.

The -t flag instructs get to also download the packages required to build
the tests for the specified packages.

The -u flag instructs get to use the network to update the named packages
and their dependencies. By default, get uses the network to check out
missing packages but does not use it to look for updates to existing packages.

The -v flag enables verbose progress and debug output.

Get also accepts build flags to control the installation. See 'go help build'.

When checking out a new package, get creates the target directory
GOPATH/src/<import-path>. If the GOPATH contains multiple entries,
get uses the first one. For more details see: 'go help gopath'.

When checking out or updating a package, get looks for a branch or tag
that matches the locally installed version of Go. The most important
rule is that if the local installation is running version "go1", get
searches for a branch or tag named "go1". If no such version exists
it retrieves the default branch of the package.

When go get checks out or updates a Git repository,
it also updates any git submodules referenced by the repository.

Get never checks out or updates code stored in vendor directories.

For more about specifying packages, see 'go help packages'.

For more about how 'go get' finds source code to
download, see 'go help importpath'.

This text describes the behavior of get when using GOPATH
to manage source code and dependencies.
If instead the go command is running in module-aware mode,
the details of get's flags and effects change, as does 'go help get'.
See 'go help modules' and 'go help module-get'.

See also: go build, go install, go clean.
	`,
}

var HelpGopathGet = &base.Command{
	UsageLine: "gopath-get",
	Short:     "legacy GOPATH go get",
	Long: `
The 'go get' command changes behavior depending on whether the
go command is running in module-aware mode or legacy GOPATH mode.
This help text, accessible as 'go help gopath-get' even in module-aware mode,
describes 'go get' as it operates in legacy GOPATH mode.

Usage: ` + CmdGet.UsageLine + `
` + CmdGet.Long,
}

var (
	getD        = CmdGet.Flag.Bool("d", false, "")
	getF        = CmdGet.Flag.Bool("f", false, "")
	getT        = CmdGet.Flag.Bool("t", false, "")
	getU        = CmdGet.Flag.Bool("u", false, "")
	getFix      = CmdGet.Flag.Bool("fix", false, "")
	getInsecure = CmdGet.Flag.Bool("insecure", false, "")
)

func init() {
	work.AddBuildFlags(CmdGet, work.OmitModFlag|work.OmitModCommonFlags)
	CmdGet.Run = runGet // 中断初始化循环
}

func runGet(ctx context.Context, cmd *base.Command, args []string) {
	if cfg.ModulesEnabled {
		// 不应发生：main.go应安装单独的模块启用get代码。
		base.Fatalf("go get: modules not implemented")
	}

	work.BuildInit()

	if *getF && !*getU {
		base.Fatalf("go get: cannot use -f flag without -u")
	}
	if *getInsecure {
		base.Fatalf("go get: -insecure flag is no longer supported; use GOINSECURE instead")
	}

	// 禁用Git本身对密码的任何提示。
	// 仅对2.3.0或更高版本有效，但避免
	// 早期版本中的提示太难了。
	// 如果用户已显式设置GIT_TERMINAL_PROMPT=1，请保留
	// 提示。
	// 见golang.org/issue/9341和golang.org/issue/12706。
	if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
		os.Setenv("GIT_TERMINAL_PROMPT", "0")
	}

	// 还禁用由生成的“ssh”子进程提示输入密码
	// Git，因为显然Git_TERMINAL_提示符不足以做到这一点。
	// 添加'-o BatchMode=yes'应该可以做到这一点。
	// None
	// 如果Git子进程将子进程分叉到后台以缓存新连接，
	// 那个孩子开着门。Git子流程退出后，
	// os/exec希望能够从stdout/stderr管道读取数据
	// 直到EOF，才能获取Git子进程在退出之前写入的所有数据。
	// 直到孩子也退出，EOF才会出现，因为孩子
	// 正在固定管道的写入端。
	// 这是不幸的，但至少出现过两次
	// （见golang.org/issue/13453和golang.org/issue/16104）
	// 当它这样做时，会让用户感到困惑。
	// 如果用户已显式设置GIT_SSH或GIT_SSH_命令，
	// 假设他们知道自己在做什么，不要踩在上面。
	// 但默认为关闭ControlMaster。
	if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
		os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no -o BatchMode=yes")
	}

	// 还有另外一个Git提示源：Git Credential Manager Core for Windows。
	// None
	// 请参阅https:
	if os.Getenv("GCM_INTERACTIVE") == "" {
		os.Setenv("GCM_INTERACTIVE", "never")
	}

	// 第一阶段。下载/更新。
	var stk load.ImportStack
	mode := 0
	if *getT {
		mode |= load.GetTestDeps
	}
	for _, pkg := range downloadPaths(args) {
		download(pkg, nil, &stk, mode)
	}
	base.ExitIfErrors()

	// 第二阶段。重新扫描包并重新评估参数列表。

	// 我们下载的代码和所有依赖它的代码
	// 需要从包缓存中逐出，以便
	// 将重新计算该信息。而不是
	// 跟踪反向依赖项信息，收回
	// 每件事
	load.ClearPackageCache()

	pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{}, args)
	load.CheckPackageErrors(pkgs)

	// 第三阶段。安装
	if *getD {
		// 只供下载。
		// 检查延迟到现在，以便下载路径
		// 并且CheckPackageErrors有机会打印错误。
		return
	}

	work.InstallPackages(ctx, args, pkgs)
}

// downloadPaths准备要传递到下载的路径列表。
// 它扩展了。。。可以扩展的模式。如果没有对手
// 对于特定模式，DownloadPath将其保留在结果列表中，
// 希望我们能从
// 首字母…-自由前缀。
func downloadPaths(patterns []string) []string {
	for _, arg := range patterns {
		if strings.Contains(arg, "@") {
			base.Fatalf("go: can only use path@version syntax with 'go get' and 'go install' in module-aware mode")
			continue
		}

		// 谨防“去拿x.go”，这是一个常见的错误。
		// 请注意，包和模块路径可能以“.go”结尾，因此仅打印错误
		// 如果参数没有斜杠或引用现有文件。
		if strings.HasSuffix(arg, ".go") {
			if !strings.Contains(arg, "/") {
				base.Errorf("go get %s: arguments must be package or module paths", arg)
				continue
			}
			if fi, err := os.Stat(arg); err == nil && !fi.IsDir() {
				base.Errorf("go get: %s exists as a file, but 'go get' requires package arguments", arg)
			}
		}
	}
	base.ExitIfErrors()

	var pkgs []string
	for _, m := range search.ImportPathsQuiet(patterns) {
		if len(m.Pkgs) == 0 && strings.Contains(m.Pattern(), "...") {
			pkgs = append(pkgs, m.Pattern())
		} else {
			pkgs = append(pkgs, m.Pkgs...)
		}
	}
	return pkgs
}

// downloadCache记录我们已经拥有的导入路径
// 在下载过程中考虑，以避免在下载时重复工作
// 有多个依赖项序列导致
// 一个特殊的包裹。
var downloadCache = map[string]bool{}

// downloadRootCache记录版本控制存储库
// 我们在下载过程中已经考虑了根目录。
// 例如，github.com/google/codesearch repo中的所有包
// 共享同一根目录（该路径的目录），我们只需要
// 运行HG命令以考虑每个存储库一次。
var downloadRootCache = map[string]bool{}

// download运行get命令的下载部分
// 对于由参数命名的包或模式。
func download(arg string, parent *load.Package, stk *load.ImportStack, mode int) {
	if mode&load.ResolveImport != 0 {
		// 调用方负责扩展供应商路径。
		panic("internal error: download mode has useVendor set")
	}
	load1 := func(path string, mode int) *load.Package {
		if parent == nil {
			mode := 0 // 不执行模块或供应商解决方案
			return load.LoadImport(context.TODO(), load.PackageOpts{}, path, base.Cwd(), nil, stk, nil, mode)
		}
		return load.LoadImport(context.TODO(), load.PackageOpts{}, path, parent.Dir, parent, stk, nil, mode|load.ResolveModule)
	}

	p := load1(arg, mode)
	if p.Error != nil && p.Error.Hard {
		base.Errorf("%s", p.Error)
		return
	}

	// loadPackage从arg推断出规范导入路径。
	// 在以下情况下使用该选项可防止滞后效应
	// 例如，在downloadCache和packageCache中。
	// 这允许调用，例如：
	// mkdir-p$GOPATH/src/github.com/user
	// cd$GOPATH/src/github.com/user
	// 去拿./foo
	// 见：golang.org/issue/9767
	arg = p.ImportPath

	// 如果这是标准库中的包，则无需执行任何操作。
	if p.Standard {
		return
	}

	// 每个包只处理一次。
	// （除非我们获取此包的测试依赖项，
	// 在这种情况下，我们希望再次处理。）
	if downloadCache[arg] && mode&load.GetTestDeps == 0 {
		return
	}
	downloadCache[arg] = true

	pkgs := []*load.Package{p}
	wildcardOkay := len(*stk) == 0
	isWildcard := false

	// 如果软件包丢失，请下载，如果使用-u，请更新。
	if p.Dir == "" || *getU {
		// 实际下载。
		stk.Push(arg)
		err := downloadPackage(p)
		if err != nil {
			base.Errorf("%s", &load.PackageError{ImportStack: stk.Copy(), Err: err})
			stk.Pop()
			return
		}
		stk.Pop()

		args := []string{arg}
		// 如果参数中包含通配符，请重新计算通配符。
		// 我们将此延迟到重新加载包之后，以便旧条目
		// 对于p，已在包缓存中替换。
		if wildcardOkay && strings.Contains(arg, "...") {
			match := search.NewMatch(arg)
			if match.IsLocal() {
				match.MatchDirs()
				args = match.Dirs
			} else {
				match.MatchPackages()
				args = match.Pkgs
			}
			for _, err := range match.Errs {
				base.Errorf("%s", err)
			}
			isWildcard = true
		}

		// 在开始之前清除所有相关的包缓存项
		// 做任何新的工作。
		load.ClearPackageCachePartial(args)

		pkgs = pkgs[:0]
		for _, arg := range args {
			// 注意：load调用loadPackage或loadImport，
			// 已经把arg推到stk上了。
			// 不要推到这里，否则stk会说arg导入arg。
			p := load1(arg, mode)
			if p.Error != nil {
				base.Errorf("%s", p.Error)
				continue
			}
			pkgs = append(pkgs, p)
		}
	}

	// 进程包，现在可能是多个包
	// 由于通配符扩展。
	for _, p := range pkgs {
		if *getFix {
			files := base.RelPaths(p.InternalAllGoFiles())
			base.Run(cfg.BuildToolexec, str.StringList(base.Tool("fix"), files))

			// 导入可能已更改，因此请重新加载。
			p = load.ReloadPackageNoFlags(arg, stk)
			if p.Error != nil {
				base.Errorf("%s", p.Error)
				return
			}
		}

		if isWildcard {
			// 报告真实的包和
			// 任何错误消息中的通配符。
			stk.Push(p.ImportPath)
		}

		// 流程依赖关系，现在我们知道它们是什么了。
		imports := p.Imports
		if mode&load.GetTestDeps != 0 {
			// 指定-t时的流程测试依赖项。
			// （但不获取测试依赖项的测试依赖项：
			// 我们总是将模式0传递给下面的递归调用。）
			imports = str.StringList(imports, p.TestImports, p.XTestImports)
		}
		for i, path := range imports {
			if path == "C" {
				continue
			}
			// 导入命名完整供应商路径时快速失败。
			// 否则，请根据测试导入的需要展开路径。
			// 请注意，p.Imports可以有p.Internal.Build.Imports之外的其他条目。
			orig := path
			if i < len(p.Internal.Build.Imports) {
				orig = p.Internal.Build.Imports[i]
			}
			if j, ok := load.FindVendor(orig); ok {
				stk.Push(path)
				err := &load.PackageError{
					ImportStack: stk.Copy(),
					Err:         load.ImportErrorf(path, "%s must be imported as %s", path, path[j+len("vendor/"):]),
				}
				stk.Pop()
				base.Errorf("%s", err)
				continue
			}
			// 如果这是测试导入，请立即应用模块和供应商查找。
			// 我们无法将ResolveImport传递到下载，因为
			// 下载基于路径的值进行缓存，
			// 因此，它必须已经是完全限定的路径。
			if i >= len(p.Imports) {
				path = load.ResolveImportPath(p, path)
			}
			download(path, p, stk, 0)
		}

		if isWildcard {
			stk.Pop()
		}
	}
}

// downloadPackage运行create或download命令
// 创建给定包的第一个副本或更新给定包的副本。
func downloadPackage(p *load.Package) error {
	var (
		vcsCmd         *vcs.Cmd
		repo, rootPath string
		err            error
		blindRepo      bool // 如果回购有异常配置，则设置
	)

	// p可以是一个实包，也可以是一个伪包，其“导入路径”为
	// 实际上是一个通配符模式。
	// 修剪包含第一个通配符的元素处的路径，
	// 希望它也适用于通配符部分。
	// 这使得“gogetrsc.io/pdf/…”在一个新的路径中工作。
	importPrefix := p.ImportPath
	if i := strings.Index(importPrefix, "..."); i >= 0 {
		slash := strings.LastIndexByte(importPrefix[:i], '/')
		if slash < 0 {
			return fmt.Errorf("cannot expand ... in %q", p.ImportPath)
		}
		importPrefix = importPrefix[:slash]
	}
	if err := checkImportPath(importPrefix); err != nil {
		return fmt.Errorf("%s: invalid import path: %v", p.ImportPath, err)
	}
	security := web.SecureOnly
	if module.MatchPrefixPatterns(cfg.GOINSECURE, importPrefix) {
		security = web.Insecure
	}

	if p.Internal.Build.SrcRoot != "" {
		// 目录存在。沿着src的路径查找签出。
		vcsCmd, rootPath, err = vcs.FromDir(p.Dir, p.Internal.Build.SrcRoot)
		if err != nil {
			return err
		}
		repo = "<local>" // 应该是不用的；与众不同

		// 仔细检查它是从哪里来的。
		if *getU && vcsCmd.RemoteRepo != nil {
			dir := filepath.Join(p.Internal.Build.SrcRoot, filepath.FromSlash(rootPath))
			remote, err := vcsCmd.RemoteRepo(vcsCmd, dir)
			if err != nil {
				// 还是继续吧。包裹存在；我们可能就是不明白
				// 回购配置（例如，异常远程协议）。
				blindRepo = true
			}
			repo = remote
			if !*getF && err == nil {
				if rr, err := vcs.RepoRootForImportPath(importPrefix, vcs.IgnoreMod, security); err == nil {
					repo := rr.Repo
					if rr.VCS.ResolveRepo != nil {
						resolved, err := rr.VCS.ResolveRepo(rr.VCS, dir, repo)
						if err == nil {
							repo = resolved
						}
					}
					if remote != repo && rr.IsCustom {
						return fmt.Errorf("%s is a custom import path for %s, but %s is checked out from %s", rr.Root, repo, dir, remote)
					}
				}
			}
		}
	} else {
		// 分析导入路径以确定版本控制系统，
		// 以及存储库根目录的导入路径。
		rr, err := vcs.RepoRootForImportPath(importPrefix, vcs.IgnoreMod, security)
		if err != nil {
			return err
		}
		vcsCmd, repo, rootPath = rr.VCS, rr.Repo, rr.Root
	}
	if !blindRepo && !vcsCmd.IsSecure(repo) && security != web.Insecure {
		return fmt.Errorf("cannot download, %v uses insecure protocol", repo)
	}

	if p.Internal.Build.SrcRoot == "" {
		// 找不到包。放入$GOPATH的第一个目录中。
		list := filepath.SplitList(cfg.BuildContext.GOPATH)
		if len(list) == 0 {
			return fmt.Errorf("cannot download, $GOPATH not set. For more details see: 'go help gopath'")
		}
		// 防止设置GOPATH=$GOROOT的人。
		if filepath.Clean(list[0]) == filepath.Clean(cfg.GOROOT) {
			return fmt.Errorf("cannot download, $GOPATH must not be set to $GOROOT. For more details see: 'go help gopath'")
		}
		if _, err := os.Stat(filepath.Join(list[0], "src/cmd/go/alldocs.go")); err == nil {
			return fmt.Errorf("cannot download, %s is a GOROOT, not a GOPATH. For more details see: 'go help gopath'", list[0])
		}
		p.Internal.Build.Root = list[0]
		p.Internal.Build.SrcRoot = filepath.Join(list[0], "src")
		p.Internal.Build.PkgRoot = filepath.Join(list[0], "pkg")
	}
	root := filepath.Join(p.Internal.Build.SrcRoot, filepath.FromSlash(rootPath))

	if err := vcs.CheckNested(vcsCmd, root, p.Internal.Build.SrcRoot); err != nil {
		return err
	}

	// 如果我们已经考虑过此存储库，请不要再这样做。
	if downloadRootCache[root] {
		return nil
	}
	downloadRootCache[root] = true

	if cfg.BuildV {
		fmt.Fprintf(os.Stderr, "%s (download)\n", rootPath)
	}

	// 检查这是否是要签出回购协议的适当位置。
	// 目标目录必须不存在或已签出repo。
	meta := filepath.Join(root, "."+vcsCmd.Cmd)
	if _, err := os.Stat(meta); err != nil {
		// 元数据文件或目录不存在。准备签出新副本。
		// 某些版本控制工具要求目标目录不存在。
		// 我们也要求这样做，只是为了避免妨碍现有的工作。
		if _, err := os.Stat(root); err == nil {
			return fmt.Errorf("%s exists but %s does not - stale checkout?", root, meta)
		}

		_, err := os.Stat(p.Internal.Build.Root)
		gopathExisted := err == nil

		// 某些版本控制工具要求目标的父级存在。
		parent, _ := filepath.Split(root)
		if err = os.MkdirAll(parent, 0777); err != nil {
			return err
		}
		if cfg.BuildV && !gopathExisted && p.Internal.Build.Root == cfg.BuildContext.GOPATH {
			fmt.Fprintf(os.Stderr, "created GOPATH=%s; see 'go help gopath'\n", p.Internal.Build.Root)
		}

		if err = vcsCmd.Create(root, repo); err != nil {
			return err
		}
	} else {
		// 元数据目录确实存在；下载增量更新。
		if err = vcsCmd.Download(root); err != nil {
			return err
		}
	}

	if cfg.BuildN {
		// 不在-n中显示标签同步；噪音比什么都重要，
		// 因为我们没有运行命令，所以找不到标记。
		// 但避免打印任何内容。
		fmt.Fprintf(os.Stderr, "# cd %s; %s sync/update\n", root, vcsCmd.Cmd)
		return nil
	}

	// 选择并同步到存储库的相应版本。
	tags, err := vcsCmd.Tags(root)
	if err != nil {
		return err
	}
	vers := runtime.Version()
	if i := strings.Index(vers, " "); i >= 0 {
		vers = vers[:i]
	}
	if err := vcsCmd.TagSync(root, selectTag(vers, tags)); err != nil {
		return err
	}

	return nil
}

// selectTag返回与给定版本最匹配的标记。
// “最近”是指不在当前版本之后的最新版本。
// 版本“goX”（或“goX.Y”或“goX.Y.Z”）匹配相同形式的标签。
// 版本“release.rN”匹配形式为“go.rN”的标记（N为浮点数）。
// 版本“weekly.YYYY-MM-DD”匹配像“go.weekly.YYYY-MM-DD”这样的标签。
// None
// 注意（rsc）：最终我们需要在这里决定一些逻辑。
// 目前，只有“go1”。这与go help get中的文档相匹配。
func selectTag(goVersion string, tags []string) (match string) {
	for _, t := range tags {
		if t == "go1" {
			return "go1"
		}
	}
	return ""
}

// checkImportPath与module.checkImportPath类似，但它禁止前导点
// 在路径元素中。这可能导致“go get”创建.git和其他VCS
// 我们以后可能运行VCS工具的地方的目录。
func checkImportPath(path string) error {
	if err := module.CheckImportPath(path); err != nil {
		return err
	}
	checkElem := func(elem string) error {
		if elem[0] == '.' {
			return fmt.Errorf("malformed import path %q: leading dot in path element", path)
		}
		return nil
	}
	elemStart := 0
	for i, r := range path {
		if r == '/' {
			if err := checkElem(path[elemStart:]); err != nil {
				return err
			}
			elemStart = i + 1
		}
	}
	if err := checkElem(path[elemStart:]); err != nil {
		return err
	}
	return nil
}
